<template>
  <!-- 此为虚拟无限-模拟-树表-->
  <div class="treeBox">
    <!-- 搜索及其返回区域 -->
    <div class="d-flex ai-center jc-between mb-2">
      <el-input v-model="searchText" size="small" placeholder="请输入"></el-input>
      <div class="flex-1 d-flex ai-center">
        <el-button type="primary" size="small" icon="el-icon-search" @click="search(searchText)"></el-button>
        <el-button type="primary" size="small" @click="backTo">上一级</el-button>
        <el-button type="primary" size="small" icon="el-icon-news" @click="showTree"></el-button>
      </div>
    </div>
    
    <!-- 试图展示区域 -->
    <div class="scrollList" ref="scrollList" @scroll="scrollChange">
      <div ref="scrollBar"></div>
      <div class="scrollBar" ref="showList">
        <div class="line d-flex ai-center jc-between" v-for="(item,i) in list" :key="'item_' + i" @click="selectChild(item,i)">
          <span :class="['ecli', activeIndex == item.sortIndex ? 'active' : '']" :title="item.name">
            <slot name="name" :data="item" v-if="slot"></slot>
            <template v-else>
              <i class="el-icon-folder-opened"></i>
              {{item.name}}
            </template>
          </span>
          <i :class="['mx-2', loadLeaf(item)]" @click.stop="toNext(item, i)"></i>
        </div>
      </div>
    </div> 

    <!-- 显示层级结构的悬浮窗 -->
    <div class="popTreeList" ref="breadList" v-if="popTreeListShow">
      <template v-if="allList.length > 1">
        <div class="d-flex ai-center mb-1 cursor" v-for="(item,i) in allList" :key="'popTreeList'+ i" @click="toTip(item)">
          <template v-if="item.name">
            <i class="el-icon-caret-bottom"></i>
            <span class="ecli" :title="item.name">{{item.name}}</span>
          </template>
        </div>
      </template>
    </div>
  </div>
</template>

<script lang="ts" setup>
// 存储节点的渲染类型约束
interface allList {
  index?: number,
  top?: number,
  start?: number,
  end?: number,
  data: any[],
  name? : string,
}

import { reactive, ref, computed, defineProps, onMounted, watch, nextTick, onUnmounted, defineEmit, useContext } from "vue";

// 导出子组件需要暴露出去的方法
const { expose } = useContext()

// 父子组件传值
let emit = defineEmit(['load','select'])

const props = defineProps({
  // 存储显示数据的字段
  data: {
    type: Array,
    default(){ return [] }
  },
  // 展示的数目，同时决定着当前树表的高度
  showCounts:{
    type: Number,
    default: 15
  },
  slot:{
    type: Boolean,
    default: false
  }
})

// 控制结构弹窗的变量
const popTreeListShow = ref(false)

// 搜索字段,用于检索当前目录的节点
const searchText = ref('');

// 搜索后高亮的节点索引(只会匹配最靠近的那一个)
const activeIndex = ref(-1)

// 当前显示节点的高度
const itemHeight = ref(30);

// 显示的条数
const showCount = ref(props.showCounts);

// 当前容器的滚动dom元素
const scrollList = ref<HTMLElement | null>(null);

// 虚拟的滚动条的dom元素
const scrollBar = ref<HTMLElement | null>(null);

// 显示节点结构的dom元素
const breadList = ref<HTMLElement | null>(null);

// 展示字段的dom元素
const showList = ref<HTMLElement | null>(null);

// 开始节点索引
const start = ref(0);

// 结束尾部索引
const end = ref(props.showCounts);

// 判断是否在进行懒加载，防止同时请求两个节点
const isLoading = ref(false)

// 存储信息展示节点（关键值）
let allList = reactive<allList[]>([{ data: [] }]);

// 记录当前节点信息
let activeItem = reactive<any>({})

// 显示展示的字段
let list:any = computed(()=> allList[allList.length - 1].data.slice(start.value, end.value))

// 监听数据的变化
watch(()=> props.data, (n:any,o:any)=>{
  allList[0].data = n;
}, { deep: true, immediate: true })

// 滚动事件
const scrollChange = ()=>{
  // 获取滚动高度
  let scrollTop = scrollList.value ? scrollList.value.scrollTop: 0;
  // 开始索引
  start.value = Math.floor( scrollTop / itemHeight.value);
  // 结束索引
  end.value = start.value + showCount.value;
  // 绝对定位对相对相对的偏移量
  showList.value ? showList.value.style.top = start.value * itemHeight.value + 'px': '0px';
}

// 判断当前加载icon
const loadLeaf = (item: any) => {
  // 当节点存在子节点时
  if(item.children && item.children.length){ return 'el-icon-caret-right' }
  // 当为懒加载数据时
  if(item.loading){ return 'el-icon-loading' }
  // 当为懒加载树节点时
  if(!item.children && item.leaf){ return 'el-icon-caret-right' }
}

// 回退后记住当前状态的方法
const reload = (model: allList) => {
  // 清空搜索值以及高亮节点
  activeIndex.value = -1;
  searchText.value = "";
  // 设置起始值
  start.value = model.start || 0;
  // 设置结束值
  end.value = start.value + showCount.value;
  // 设置当前展示高度
  scrollBar.value ? scrollBar.value.style.height = itemHeight.value * allList[allList.length - 1].data.length + 'px' : '';
  // 设置滚动高度
  scrollList.value ? scrollList.value.scrollTop = model.top || 0: '';
  // 结构目录跳转到最右侧
  nextTick(() => {
    breadList.value ? breadList.value.scrollTop = breadList.value.scrollHeight : '';
  });
}

// 返回上一级
const backTo = () =>{
  // 删除最后一个存储的节点，然后继承当前节点的高度
  if(allList.length > 1){ allList.pop(); }
  // 当节点只剩下最后一个的时候，关闭弹窗
  if(allList.length == 1){ popTreeListShow.value = false; }

  reload(allList[allList.length - 1]) 
}

// 进入下一个节点的公共方法
const nextToChild = (item:any, index: number) => {
  isLoading.value = false
  // 进入下一节点之前记录当前节点的位置
  let model = allList[allList.length - 1];
  model.top = (scrollList.value ? scrollList.value.scrollTop: 0);
  model.end = end.value;
  model.start = start.value;
  model.index = index;
  model.name = item.name;

  // 将下一节点的信息存入
  allList.push({
    data: item.children
  })

  // 重置高度和索引
  reload(allList[allList.length - 1]); 
}

// 进入下一级
const toNext = (item: any, index: number) =>{
  // 记录当前节点的信息，利用引用关系进行数据修改
  activeItem = item;
  // 防止在加载的过程中请求下一节点，以防出现节点混乱
  if(isLoading.value){ return }
  // 懒加载函数
  if(!item.children && item.leaf){ item.loading = true }

  if(item.loading){
    isLoading.value = true
    emit('load', item, (val:any)=>{
      item.children = val || [];
      item.loading = false;
      nextToChild(item, index);
    })
  }else{
    nextToChild(item, index);
  }
}

// 展示树结构节点弹窗
const showTree = () => {
  if(allList.length > 1){
    popTreeListShow.value = !popTreeListShow.value;
    nextTick(() => {
      setTimeout(()=> { breadList.value ? breadList.value.style.transform = 'scale(1,1)' : ''; }, 200)
      breadList.value ? breadList.value.scrollTop = breadList.value.scrollHeight : '';
    });
  }else{
    popTreeListShow.value = false;
  }
}

// 选中节点事件
const selectChild = (val:any, i: number) =>{
  // 杜绝没必要的for循环
  if(String(val.sortIndex) == 'undefined'){
    let model = allList[allList.length - 1];
    // 给每一个值赋予索引
    model.data.forEach((v:any,i:number)=>{
      v.sortIndex = i
    })
  }
  // 高亮当前选中的节点
  activeIndex.value = start.value + i;
  // 将当前选中的节点信息返回给父组件
  emit('select', val)
}

// 查询事件
const search  = (val:string | number, type = false) => {
  // 搜索前清空上一次的查询索引
  activeIndex.value = -1;

  let model = allList[allList.length - 1];
  // 给每一个值赋予索引
  model.data.forEach((v:any,i:number)=>{
    v.sortIndex = i
  })
  // 将匹配上的索引赋值给高亮节点
  let numIndex = -1;
  if(typeof val == 'number'){
    numIndex = val;
  }else{
    numIndex = model.data.findIndex((v:any)=> v.name == val)
  }
  

  // 定位到当前高度
  if(numIndex > -1){
    start.value = numIndex;
    end.value = start.value + showCount.value;
    // 高亮的索引
    activeIndex.value = numIndex;

    scrollList.value ? scrollList.value.scrollTop = numIndex * itemHeight.value || 0: '';
    showList.value ? showList.value.style.top = numIndex * itemHeight.value + 'px': '0px';

    // 默认高亮后，触发节点选中事件
    if(type){ emit('select', model.data[numIndex] ) }
  }
}

// 根据结构目录进入指定路径
const toTip = (item: allList) => {
  // 删除掉后续的所有的节点
  let index = allList.findIndex((v: allList)=> v.name == item.name && v.index == item.index && v.start == item.start);
  if(index > -1){
    allList.splice(index + 1, allList.length - 1)
  }
  // 当索引小于两个时关闭当前弹窗
  if(allList.length < 2){ popTreeListShow.value = false; }
  // 重置高度和索引
  reload(item);
}

// 默认高亮的节点信息
const findActive = (val: string | number) =>{
  search(val, true)
}

// 重新加载树节点的方法(整体更新/只更新当前值)
const reloadTree = (val: any, type = true,) =>{
  if(type){ 
    // // 整体更新
    allList = [{ data : [] }]; 
    allList[0].data = val || [];
    // 修改起始值，触发计算属性的变化
    start.value = -1;
    reload(allList[allList.length - 1]);
  }else{
    // 只更新当前节点
    let model = allList[allList.length - 1];
    // 更新原始节点
    activeItem.children = val;
    // 更新当前显示数据
    model.data = val;
    // 修改起始值，触发计算属性的变化
    start.value = -1;
    reload(allList[allList.length - 1]);
  }
}

// 生成初始化高度
onMounted(()=>{
  // 设置当前滚动高度以及滚动条高度
  scrollList.value ? scrollList.value.style.height = itemHeight.value * showCount.value + 'px': '';
  scrollBar.value ? scrollBar.value.style.height = itemHeight.value * props.data.length + 'px' : '';
})

// 向外暴露的方法，用来默认高亮节点的方法
expose({
  // 默认高亮节点
  findActive,
  // 重新渲染节点
  reloadTree
})

// 组件销毁以后，清空存储的值
onUnmounted(()=>{
  allList = [];
  scrollList.value = null;
  scrollBar.value = null;
  breadList.value = null;
})

</script>

<style lang="scss" scoped>
.treeBox{
  position: relative;
  padding: 0px 10px;
  box-sizing: border-box;
  .popTreeList{
    position: absolute;
    width: 300px;
    max-height: 40vh;
    border: 1px solid #ccc;
    border-radius: 4px;
    padding: 15px 10px;
    box-sizing: border-box;
    right: -300px;
    top: 0px;
    overflow-y: auto;
    transform: scale(0);
    transition: all 0.6s;
  }
}
.border_list{
  border-bottom: 1px dashed #ccc;
  white-space: nowrap;
  
  .cursor{
    cursor: pointer;
    word-break: break-all;
  }
}
.scrollList{
  position: relative;
  overflow-y: auto;
  box-sizing: border-box;
  .scrollBar{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    .line{
      width: 100%;
      line-height: 30px;
      cursor: pointer;
      .active{
        color: rgb(64, 158, 255);
      }
    }
  }
}

.ecli{
  word-break: break-all;
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
}
</style>